{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Voltages and Currents in Circuits\n", "The scikit-rf `Circuit` object allows one to deduce the voltages and currents at all the intersections of the circuit for a given power (and phase) excitation at circuit's ports. Here, few examples are examined, in order to clarify the conventions used for current directions. " ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "%matplotlib inline" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# standard imports\n", "import numpy as np\n", "\n", "import skrf as rf" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "rf.stylely()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## A simple transmission line\n", "To start, let's assume a simple transmission line excited by a generator. Let's assume the generator is matched to the line ($Z_s=Z_0$) and the line connected to a matched load ($Z_0=Z_L$), all 50 Ohm. \n", "\n", "\n", "\n", "What is the RF currents and voltages at the input and output of this line for a given input power (and phase)?" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "P_f = 1 # forward power in Watt\n", "Z = 50 # source internal impedance, line characteristic impedance and load impedance\n", "L = 10 # line length in [m]\n", "\n", "freq = rf.Frequency(2, 2, 1, unit='GHz')\n", "line_media = rf.media.DefinedGammaZ0(freq, z0=Z) # lossless line medium\n", "line = line_media.line(d=L, unit='m', name='line') # transmission line Network" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Assuming the source generates an input power of $P_f$ with a phase $\\phi$, with such a line the voltage and current at the entrance of the line are:\n", "\n", "$$\n", "V_{in} = \\sqrt{2 Z_0 P_f} e^{j \\phi}\n", "$$\n", "\n", "$$\n", "I_{in} = \\sqrt{2 \\frac{P_f}{Z_0}} e^{j \\phi}\n", "$$\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "V_in = np.sqrt(2*Z*P_f)\n", "I_in = np.sqrt(2*P_f/Z)\n", "print(f'Input voltage and current: {V_in} V and {I_in} A')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The voltage and current evolve along the transmission line. The voltage and current at the output of the line can be calculated using the transmission line tools provided with scikit-rf:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "theta = rf.theta(line_media.gamma, freq.f, L) # electrical length\n", "V_out, I_out = rf.tlineFunctions.voltage_current_propagation(V_in, I_in, Z, theta)\n", "print(f'Output voltage and current: {V_out} V and {I_out} A')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Let's perform the same calculation using `Circuit`. First, one needs to define the circuit, that is to create input/output ports and to connect these ports to the transmission line Network we've already created. Then, we can build the circuit: " ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "port1 = rf.Circuit.Port(frequency=freq, name='port1', z0=50)\n", "port2 = rf.Circuit.Port(frequency=freq, name='port2', z0=50)\n", "cnx = [\n", " [(port1, 0), (line, 0)],\n", " [(port2, 0), (line, 1)]\n", "]\n", "crt = rf.Circuit(cnx)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "It's always a good practice to check if the circuit's graph is as expected. In this case, the graph is pretty simple: 2 ports connected to a 2-ports Network. " ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "crt.plot_graph(network_labels=True, edge_labels=True, inter_labels=True)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "`Circuit` provides two methods to determine voltages and currents at the circuit input/output ports (also known as \"external ports\"). These methods take as input the power and phase inputs at each ports:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "power = [1, 0] # 1 Watt at port1 and 0 at port2\n", "phase = [0, 0] # 0 radians" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "V_at_ports = crt.voltages_external(power, phase)\n", "print(V_at_ports)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "I_at_ports = crt.currents_external(power, phase)\n", "print(I_at_ports)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The results are similar to the previous, except the sign of the current at port2 which is reversed. \n", "\n", "This is normal, as the `currents_external()` method defines the positive direction of a current as the direction which \"enters\" into the circuit. More details about this convention at the bottom of this example." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## A coaxial T with different characteristic impedances\n", "As a more advanced example, we've built in a full-wave software (here ANSYS HFSS, but other are fine too) the following structure: a coaxial T, defined with different coaxial cross-sections (and such different characteristic impedances). \n", "\n", "" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "All the three ports are excited with different power and phase to complicate a bit, as the following:\n", "\n", "| port | power [W] | phase [deg] |\n", "| --- | --- | --- |\n", "| #1 | 1 | -10 |\n", "| #2 | 2 | -20 |\n", "| #3 | 3 | +60 |\n", "\n", "### Full-wave reference solution\n", "The voltages and currents are evaluated in the full-wave software directly. Voltage is obtained by integrating the E fields along a straight line going from the inner to the outer conductors in the port's cross-sections. Current is obtained by integrating the H fields along a circle enclosing the inner conductor in port's cross-sections. Note the circles are directed in order to define the positive current direction as the direction inward the ports (right-hand rule). \n", "\n", "The full-wave solutions are given here for reference, at 3 frequencies:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import pandas as pd # convenient to read .csv files\n", "\n", "pd.read_csv('circuit_vi_HFSS_Voltages.csv')" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "pd.read_csv('circuit_vi_HFSS_Currents.csv')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### In a Circuit simulator\n", "The voltages and currents can also be derived using a Circuit simulator, like for example:\n", "\n", "\n", "\n", "(where, again, the current probes are oriented in such a way that the current is positive when flowing in the Network). This gives essentially the same results:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "pd.read_csv('circuit_vi_Designer_Voltages.csv')" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "pd.read_csv('circuit_vi_Designer_Currents.csv')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### With `Circuit`\n", "Now let's build the same circuit with scikit-rf `Circuit`:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Importing the 3-port .s3p file exported from full-wave simulation\n", "coaxial_T = rf.Network('circuit_vi_Coaxial_T.s3p')\n", "# pay attention to the port's characteristic impedance\n", "# it should match the Network characteristic impedances otherwise this will generate mismatches\n", "port1 = rf.Circuit.Port(coaxial_T.frequency, 'port1', coaxial_T.z0[:,0])\n", "port2 = rf.Circuit.Port(coaxial_T.frequency, 'port2', coaxial_T.z0[:,1])\n", "port3 = rf.Circuit.Port(coaxial_T.frequency, 'port3', coaxial_T.z0[:,2])\n", "# connection list\n", "cnx = [\n", " [(port1, 0), (coaxial_T, 0)],\n", " [(port2, 0), (coaxial_T, 1)],\n", " [(port3, 0), (coaxial_T, 2)]\n", "]\n", "# building the circuit\n", "crt = rf.Circuit(cnx)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# let's check if our connection list is correctly defined\n", "crt.plot_graph(network_labels=True, edge_labels=True, inter_labels=True)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The voltages and currents at the ports for the given excitation is:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "power = [1, 2, 3] # input power in watts at ports 1, 2 and 3\n", "phase = np.deg2rad([-10, -20, +60]) # input phase in rad at ports 1, 2 and 3\n", "\n", "voltages = crt.voltages_external(power, phase)\n", "currents = crt.currents_external(power, phase)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# just for a better rendering in the notebook\n", "pd.concat([\n", " pd.DataFrame(np.abs(voltages), columns=['mag V1', 'mag V2', 'mag V3'], index=crt.frequency.f/1e6),\n", " pd.DataFrame(np.angle(voltages, deg=True), columns=['Phase V1', 'Phase V2', 'Phase V3'], index=crt.frequency.f/1e6)\n", "], axis=1)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# just for a better rendering in the notebook\n", "pd.concat([\n", " pd.DataFrame(np.abs(currents), columns=['mag I1', 'mag I2', 'mag I3'], index=crt.frequency.f/1e6),\n", " pd.DataFrame(np.angle(currents, deg=True), columns=['Phase I1', 'Phase I2', 'Phase I3'], index=crt.frequency.f/1e6)\n", "], axis=1)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "These results matches well the one given by the full-wave calculations, hourra. " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## external vs internal ports? \n", "You have maybe noticed in the `Circuit` documentation that we often talk about \"internal\" or \"inner\" ports, and of \"external\" or \"outer\" ports. The external ports corresponds to the `Circuit.Port()` Networks defined when building a `Circuit`. The internal ports are all the other connexions inside the `Circuit`\n", "\n", "The `Circuit` algorithm allows one to have access to the voltages and current at the internal connexions inside the circuit. In the previous examples, there is not too much internal ports as we've connected a N-port directly to external ports. However, in more complex usages we can have a lot (look to the other `Circuit` examples).\n", "\n", "In `Circuit`, voltages and currents are peak values. While voltages are defined in a non-ambiguous manner, positive currents can be defined in a way or another, leading to a 180 degree ambiguity. To solve this, we have chosen the following definition: internal currents are defined such as they are measured positive when flowing into Networks. \n", "\n", "Hence, you find that \"external\" current are sign opposite of \"internal\" ones at the corresponding ports, because the internal currents are actually flowing into the ports Networks." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# internals currents (currents at all connexions)\n", "# in this example, there are 6 internal connexions (3 pairs of connexions)\n", "crt.currents(power, phase)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# This gives the indices of the \"external\" ports\n", "crt.port_indexes" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# So we can keep only \"external\" ports\n", "crt.currents(power, phase)[:, crt.port_indexes]" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# note the sign difference due to the convention chosen for internal ports\n", "crt.currents_external(power, phase)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The figure below illustrates these differences using the previous example:\n", "\n", "" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Voltages and Currents in complex Circuits\n", "### power-waves and traveling-waves\n", "In `scikit-rf`, the calculation of `Voltages` and `Currents` relies on the Scattering Parameters (`S-parameters`) of the `Circuit`. The default definition of `S-parameters` uses [_power-waves_](https://scikit-rf.readthedocs.io/en/latest/examples/networktheory/Working%20with%20Complex%20Characteristic%20Impedances.html), expressed as:\n", "$$ s_{ij} = \\frac{b_i}{a_j} $$\n", "where $b_i$ and $a_j$ represent the output and input power waves at ports `i` and `j` in a given `Network`, respectively.\n", "\n", "As an example, the methods `Circuit._b()` and `Circuit._a()` provide support for calculating these output and input power waves, although this is often unnecessary for typical use cases." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# To obtain the input/output power waves under given phase and amplitude, you can use:\n", "a = crt._a(crt._a_external(power, phase))\n", "b = crt._b(a)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "From the `S-parameters` definition, we can determine the forward (input) traveling wave $V^+_i$ and the backward (output) traveling wave $V^-_j$ at ports `i` and `j` in a given `Network` as:\n", "$$V_i^+ = a_i \\sqrt{\\Re({Z_0}_i)} $$\n", "$$V_j^- = b_j \\sqrt{\\Re({Z_0}_j)} $$\n", "Here, ${Z_0}_i$ and ${Z_0}_j$ are the characteristic impedances at port `i` and `j`, respectively." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Voltage and Traveling waves\n", "In transmission line theory, the total voltage and total current are functions of the incident and reflected voltage wave amplitudes. Similarly, the `Voltage` and `Current` at each node in a `Circuit` can be calculated using the forward (input) and backward (output) traveling wave at the components' ports that form the node.\n", "\n", "For simple series connections, such as when port `m` of `Network` $A$ is connected to port `n` of `Network` $B$, the total `Voltage` at the node can be calculated using the same principles as for transmission lines:\n", "$$V=V_m^- + V_n^-$$\n", "Here, the $V_m^-$ and $V_n^-$ are the backward (output) traveling waves at ports `m` and `n`, respectively." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Voltage under impedance mismatch\n", "When the port characteristic impedances of `Network` $A$'s port `m` (${Z_0}_{Am}$) and `Network` $B$'s port `n` (${Z_0}_{Bn}$) are different, impedance mismatch occurs. This requires the use of the transmission coefficient $\\Tau_{nm}$ to correct the mismatch.\n", "\n", "When a voltage wave is transmitted from `Network` $A$' port `m` to `Network` $B$'s port `n`, `Network` $A$ can be treated as the source and `Network` $B$ as the load. The transmission coefficient $\\Tau_{nm}$ from `m` to `n` can then be used to adjust for the mismatch:\n", "$$ \\Tau_{nm} = \\frac{2 Z_{Load}}{Z_{Load}+Z_{Source}} = \\frac{2 {Z_0}_{Am}}{{Z_0}_{Am}+{Z_0}_{Bn}}$$" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Complex Circuits with Parallel Connections\n", "In more complex `Circuits`, such as those with parallel connections, multiple ports may converge at a node, each potentially having a different characteristic impedance. Despite the added complexity, the approach of using the transmission coefficient $\\Tau$ remains effective.\n", "\n", "By selecting one `Network`'s characteristic impedance $Z_l$ as the source impedance and treating the combined characteristic impedance of the other $k$ parallel `Networks` as the load impedance, the actual transmission coefficient for port `l` can be determined:\n", "$$ \\frac{1}{Z_{Source}}=\\sum_{i=0}^{k}{\\frac{1}{Z_{i}}}-\\frac{1}{Z_l}$$\n", "This approach allows for the calculation of the effective impedance at the node, which can then be used to adjust for impedance mismatches and ensure accurate `Voltage` and `Current` calculations in complex parallel `Networks` configurations." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Example\n", "The [Wilkinson Power Divider](https://scikit-rf.readthedocs.io/en/latest/examples/circuit/Wilkinson%20Power%20Splitter.html) provides an example of voltage and current calculations in complex circuits. It demonstrates the calculation for both purely series-connected `Circuit` using splitters and more complex `Circuit` that involve component parallelization, yielding consistent results in both scenarios." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [] } ], "metadata": { "kernelspec": { "display_name": "Python 3.10.6 ('.venv': venv)", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.10.6" }, "vscode": { "interpreter": { "hash": "637c323cf467337602e9974a89cb4d3fc95fac3ef875a73e62754f8e768d8de7" } } }, "nbformat": 4, "nbformat_minor": 4 }